﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Diagnostics;

namespace IntelliLance.Math.Parser
{
    /// <summary>
    /// Represents the default concrete implementation of the IMathParser interface.
    /// </summary>
    /// <remarks>Use this class  to parse an arithmetic expression and convert it into a queue of a successive tokens 
    /// that can be manipulated later in a particular way to reproduce the underlying expression in different order that 
    /// can be proceeded by the caller. This class should be used in conjunction with an instance of ITokenProcessor
    /// (passed via constructor) that is used to process a token each time the parser encounters one.</remarks>
    public class MathParser : IMathParser
    {
        #region Fields

        private readonly MathParserOptions _options;
        private readonly ITokenProcessor _processor;
        private readonly VariableDictionary _variables;
        private readonly FunctionsDictionary _functions;
        private int pos = 0;
        private Token _currentToken;
        internal const string _defaultOp = "+-*,/\\%!=";
        private const string _defaultSep = "+-,.";

        #endregion

        #region Properties
        private Token CurrentToken
        {
            get
            {
                return _currentToken;
            }
            set
            {
                _currentToken = value;
                ProcessCurrentToken();
            }
        }

        public FunctionsDictionary FunctionDefinitions
        {
            get
            {
                return _functions;
            }
        }

        public VariableDictionary VariableDefinitions
        {
            get
            {
                return _variables;
            }
        }
        #endregion

        #region Constructors

        public MathParser(ITokenProcessor processor) :
            this(MathParserOptions.Default, processor) { }

        public MathParser(MathParserOptions options, ITokenProcessor processor)
        {
            if (options == null)
                ThrowHelpers.ThrowNullArgument("options");

            if (processor == null)
                ThrowHelpers.ThrowNullArgument("processor");

            this._processor = processor;
            this._options = options;

            var comparer = StringComparer.Create(CultureInfo.InvariantCulture, options.IgnoreCase);

            this._variables = new VariableDictionary(comparer);
            this._functions = new FunctionsDictionary(comparer);
            this._variables = new VariableDictionary(comparer);
        }

        #endregion

        #region Methods


        private void ParseInput(string input, ref int pos)
        {
            int maxPos = input.Length - 1;
            while (pos <= maxPos)
            {
                if (ParseWhiteSpace(input, ref pos))
                    continue;

                if (ParseRightParenthesis(input, ref pos))
                    continue;

                if (ParseBinaryExpression(input, ref pos))
                    continue;

                if (ParseOperator(input, ref pos))
                    continue;

                if (ParseConstant(input, ref pos))
                    continue;

                if (ParseFunction(input, ref pos))
                    continue;

                if (ParseVariable(input, ref pos))
                    continue;

                pos++;
            }


        }

        private bool ParseRightParenthesis(string input, ref int pos)
        {
            if (input[pos] == ')')
            {
                CurrentToken = new RightParenthesisToken { TokenCode = ")" };
                pos++;
                return true;
            }
            return false;
        }

        private void ProcessCurrentToken()
        {
            this._processor.ProcessToken(new TokenContext(CurrentToken, this._options, pos));
        }


        private void ParseInput(string input)
        {
            pos = 0;
            this.ParseInput(input, ref pos);
        }

        private bool ParseConstant(string input, ref int pos)
        {
            var currentPos = pos;
            var number = string.Empty;
            bool flag = false;

            while (currentPos <= input.Length - 1)
            {
                var c = input[currentPos];
                if (char.IsDigit(c) || _defaultSep.Any(s => s == c))
                {
                    number += c;
                }
                else
                    break;
                currentPos++;
            }

            number = number.TrimEnd(',', '.');
            number = number.TrimStart(',', '.');
            flag = pos != currentPos;

            var match = Regex.Match(number, @"^[-+]?[0-9]+([\.\,][0-9]{1})?$");

            if (flag && match.Success)
            {
                ParseConstantValue(match.Value, pos);
            }

            pos += number.Length;
            return flag;

        }

        private void ParseConstantValue(string number, int pos)
        {
            object value = null;
            var type = this.GetConstantType(number, ref value);

            CurrentToken = new ConstantToken
            {
                Value = value,
                Type = type
            };
            Debug.WriteLine("Found constant value {0} at position {1}", number, pos);
        }

        private Type GetConstantType(string input, ref object value)
        {
            int i;
            if (int.TryParse(input, out i))
            {
                value = i;
                return typeof(int);
            }

            double d2;
            if (double.TryParse(input, out d2))
            {
                value = d2;
                return typeof(double);
            }

            decimal d;
            if (decimal.TryParse(input, out d))
            {
                value = d;
                return typeof(decimal);
            }
            return typeof(int);

        }



        private bool ParseBinaryExpression(string input, ref int pos)
        {
            bool flag = false;
            var currentPos = pos;
            if (ParseLeftParenthesis(input, ref currentPos))
            {
                flag = true;
            }

            pos = currentPos;
            return flag;
        }

        private void ParseToRightParenthesis(string input, ref int currentPos)
        {
            ParseInput(input, ref currentPos);
        }


        private bool ParseLeftParenthesis(string input, ref int currentPos)
        {
            if (input[currentPos] == '(')
            {
                currentPos++;
                CurrentToken = new LeftParenthesisToken { TokenCode = "(" };
                return true;
            }
            return false;
        }


        private bool ParseWhiteSpace(string input, ref int pos)
        {
            var currentPos = pos;
            while (currentPos <= input.Length - 1 && char.IsWhiteSpace(input[currentPos]))
            {
                currentPos++;
            }

            bool flag = pos != currentPos;
            pos = currentPos;
            return flag;
        }

        private bool ParseFunction(string input, ref int pos)
        {
            bool flag = false;
            int currentPos = pos;
            string funcName = string.Empty;
            flag = IsValidFunctionName(input, pos, ref currentPos, ref funcName);
            if (flag)
            {
                int paramsCount = 1;
                int parenthesesCount = 0;
                while (currentPos <= input.Length - 1)
                {
                    var c = input[currentPos];
                    if (c == '(')
                        parenthesesCount++;
                    else if (c == ')')
                    {
                        if (parenthesesCount == 1)
                            break;
                        parenthesesCount--;
                    }
                    if (c == ',' && parenthesesCount == 1)
                        paramsCount++;
                    currentPos++;
                }

                var func = FunctionDefinitions[funcName];
                Debug.WriteLine("Found function {0} at position {1}", funcName, pos);
                CurrentToken = new FunctionToken
                {
                    Value = funcName,
                    ParametersCount = paramsCount,
                    Function = func.Method,
                    Expression = func.Expression
                };
            }
            pos += funcName.Length;
            return flag;

        }

        private bool IsValidFunctionName(string input, int pos, ref int currentPos, ref string funcName)
        {
            while (currentPos <= input.Length - 1)
            {
                currentPos++;
                var c = input.Substring(pos, currentPos - pos);
                if (_functions.ContainsKey(c))
                {
                    funcName = c;
                    return true;
                }
            }
            return false;
        }

        private bool ParseOperator(string input, ref int pos)
        {
            bool flag = false;
            
            var c = input[pos];
            if (_defaultOp.Any(o => c == o))
            {
               var flag1 = IsUnOperator(c);
               if (c == ',')
                    CurrentToken = new FunctionParameterSeparatorToken { Value = "," };
                else 
                    CurrentToken = new OperatorToken { Value = c.ToString(), IsLeftAssociative = c != '=' || c != '^', IsBinary = !flag1 };
                Debug.WriteLine("Found operator {0} at position {1}", input[pos], pos);
                pos++;
                flag = true;
            }

            return flag;
        }

        private bool IsUnOperator(char @operator)
        {
            var flag = (CurrentToken == null
                || ((CurrentToken is OperatorToken || CurrentToken is LeftParenthesisToken ))
                && (_defaultOp.Any(c => c == @operator)));
            return flag;
        }

        private bool ParseVariable(string input, ref int pos)
        {
            bool flag = false;
            int currentPos = pos;
            string tempName = string.Empty;
            string varName = string.Empty;
            Type type = null;
            if (input[currentPos] != '@')
            {
                return false;
            }
            else
                currentPos++;
            flag = IsValidVariableName(input, ref currentPos, ref tempName, ref varName, ref type);


            if (flag)
            {
                CurrentToken = new VariableToken { Value = varName, Type = type };
                Debug.WriteLine("Found variable {0} at position {1}", varName, pos);
            }
            pos += varName.Length;
            return flag;
        }

        private bool IsValidVariableName(string input, ref int currentPos, ref string tempName, ref string varName, ref Type type)
        {
            bool flag = false;
            while (currentPos <= input.Length - 1)
            {
                tempName += input[currentPos];
                if (this.IsValidIdentifier(tempName))
                {
                    varName = tempName;
                    type = VariableDefinitions[tempName];
                    return true;
                }
                else if (_options.InferVariables && Regex.IsMatch(tempName, @"^[A-Za-z_][a-zA-Z_0-9]*[^\s]$"))
                {
                    if (flag)
                        break;
                    type = typeof(int);
                    varName = tempName;
                    flag = true;
                }
                currentPos++;
            }
            return false;
        }

        private bool IsValidIdentifier(string c)
        {
            return _variables.ContainsKey(c);
        }

        #endregion

        #region IMathParser Members
        public Queue<Token> Evaluate(string input)
        {
            if (input == null)
                ThrowHelpers.ThrowNullArgument("input");

            if (input == string.Empty)
                ThrowHelpers.ThrowEmptyString("input");

            ParseInput(input);
            var result = _processor.Eval();
            return result;
        }

        public void Optimize(MathExpressionVisitor visitor)
        {
            visitor.Accept(this);
        }

        #endregion
    }
}
